import copy
import math
import random

from simpy import Environment, Resource

from util import strutil
from x_pc.core import System
from x_pc.core.center import Center
from x_pc.core.system_context import SysDataCtx
from x_pc.db.command.model import CommandRouteStep
from x_pc.db.common.model import WorkType
from x_pc.db.sf_data import WorkCenterLoc, WorkCenterLocType, Sku
from x_pc.exceptions import PcException
from x_pc.helpers import can_work_max_time
from x_pc.model.model import TaskCtx, TaskProcess, Task, check_fix_task, TaskStatus, PackType, WorkTypeEnum, TaskSource, \
    DeviceType


class CenterDevice(Center, System):
    """
    工作中心设备
    """
    resource: Resource = None

    def __init__(self, env: Environment, name: str, ware_house, sys_data: SysDataCtx, kwargs):
        super().__init__(env, name, kwargs)
        self.resource = Resource(env)
        self.ware_house = ware_house
        self.sys_data = sys_data

    def run_task(self, task_ctx: TaskCtx):
        task: Task = task_ctx.task
        process: TaskProcess = task.process
        # 当 当前设备不可用时，循环等待
        while True:
            pending_list = self.sys_data.get_pending_device(self.device_type(), self.device_code())
            if can_work_max_time(self.env.now, 0, pending_list):
                break
            else:
                yield self.env.timeout(1)
        #
        with self.resource.request() as req:
            yield req
            source_loc: WorkCenterLoc = self.source_loc()
            # 处理前面任务的终点
            if task.pre_process().ext_resource_type.is_movable():
                task.pre_process().end_pos = source_loc.loc
            # 工作类型 (耗时、拆板、GTP)
            work_type: WorkType = process.ext_work_type
            origin_pack_type = PackType(work_type.origin_pack_type)
            target_pack_type = PackType(work_type.target_pack_type)
            # 记录初始数据
            process.begin_ts = self.env.now
            process.begin_pos = source_loc.loc
            process.set_origin_pack_type(task.pre_process().target_pack_type)
            process.set_origin_sku_num(task.pre_process().target_sku_num)
            process.operate_pack_type = process.ext_work_type.operate_pack_type
            process.operate_pack_num = 1
            #
            process.device_type = self.device_type()
            process.device_code = self.device_code()
            cost: float = self.cost(work_type.work_type)
            if cost > 3000:
                msg = f'[PC] Center耗时异常 task: {task.task_id}, route: {task.ext_route.name}({task.ext_route.id}), workType: {work_type.work_type}'
                self.sys_data.logger.warning(msg)
            yield self.env.timeout(cost / self.sys_data.time_cost_ratio)
            process.end_ts = self.env.now
            # TODO: 目的位置 先随机选择一个  (当前有GTP场景1个源位置3个目标位置，ZHC有两个位置轮流使用)
            if source_loc.loc_type_enum() in (WorkCenterLocType.SINK, WorkCenterLocType.SOURCE_SINK):
                end_pos: WorkCenterLoc = source_loc
            else:
                end_pos: WorkCenterLoc = random.choice(self.sink_loc())
            process.end_pos = end_pos.loc
            # FIXME: 理论上  这边不能处理数据
            # TODO: 包装类型、及数量的 转换
            if origin_pack_type != target_pack_type:
                msg = (f'[PC] 包装类型转换 task: {task.task_id}, name: {work_type.name}, '
                       f'from: ({origin_pack_type.value}){origin_pack_type.name}, '
                       f'to: ({target_pack_type.value}){target_pack_type.name}')
                self.sys_data.logger.info(msg)
                process.target_pack_type = target_pack_type.value
                # FIXME: task.target_sku_num ???
                # process.target_sku_num = task.target_sku_num
                # FIXME: 在工作台要拆分为几个任务就填几 ？？？
                process.operate_pack_num = 1
            # ZBJL栈板拣料 为该任务最后一个设备。在此处处理该逻辑
            if work_type.work_type == WorkTypeEnum.ZBJL.value:
                msg = f'[PC] [ZBJL栈板拣料] 栈板->纸箱 task: {task.task_id}'
                self.sys_data.logger.info(msg)
                process.target_sku_num = task.target_sku_num
                process.ext_target_lpn = f'v{0}_{strutil.random_str(10)}'[:10]
            # 当进行 GTP作业时，会产生新的LPN
            elif work_type.work_type == WorkTypeEnum.GTP.value:
                msg = f'[PC] [GTP作业] 自制箱->自制箱 task: {task.task_id}'
                self.sys_data.logger.info(msg)
                # GTP拣选时，会修改PCS数
                process.target_sku_num = task.target_sku_num
                # XXX: 此时 任务的原始(当前)PCS数也发生变化
                task.origin_sku_num = task.target_sku_num
                # 产生新的LPN
                process.ext_target_lpn = f'v{0}_{strutil.random_str(10)}'[:10]
            # 针对在库任务，栈板 多DC场景
            if task.ext_source == TaskSource.INTERIOR and task.is_master_dc_task and task.origin_pack_type == PackType.ZB.value:
                if self.equ_type == DeviceType.ZHC.value:
                    process.ext_target_lpn = f'v{0}_{strutil.random_str(10)}'[:10]
                    pidx = task.process_list.index(process)
                    for ot in task.ext_group_tasks[1:]:
                        ot.run_status = TaskStatus.RUNNING
                        copy_plist = copy.deepcopy(task.process_list[:pidx + 1])
                        ot.process_list = copy_plist + ot.process_list[pidx + 1:]
                        ot_process = copy_plist[-1]
                        ot_process.ext_target_lpn = f'v{0}_{strutil.random_str(10)}'[:10]
                        # 任务还未执行完毕
                        if pidx + 1 < len(ot.process_list):
                            msg = f'[PC] 新任务 task: {task.task_id}'
                            self.sys_data.logger.info(msg)
                            self.ware_house.distribute_task(TaskCtx(ot))
        check_fix_task(task)
        # 若任务还未完成，继续分发
        if TaskStatus.DONE != task.run_status:
            # ----------------- 当前栈板只会有: 1.栈板直发; 2.栈板PTX任务 ----------------> 理论上 不会再走这块的逻辑了
            # 换包规【拆板作业】, 栈板 -> 件箱，需要任务拆分(可能多个)
            if task.ext_source == TaskSource.DELIVERY and origin_pack_type == PackType.ZB and target_pack_type == PackType.ZX and not task.ext_ptx:
                self.process_zb_zx_split(task)
                # 处理多DC场景数据 (这块仅需处理 发货场景即可)
                if task.ext_source == TaskSource.DELIVERY and task.is_master_dc_task:
                    # TODO: 标注一下 需要拆包的纸箱
                    ext_split_list = []
                    for t in task.ext_group_tasks:
                        t: Task
                        if t.target_sku_num > t.ext_casecant and t.target_sku_num % t.ext_casecant > 0:
                            ext_split_list.append(t.target_sku_num % t.ext_casecant)
                    ext_split_box = math.ceil(sum(ext_split_list) / task.ext_casecant)
                    self.sys_data.logger.info(
                        f'[PC] [多DC场景] 需要额外的箱子: {ext_split_box}, 需拆分成的箱子: {ext_split_list}')
                    # 仅处理 非master dc任务即可
                    if Sku.gen_uk(task.sku) not in self.sys_data.nosplit_skus:
                        for otask in task.ext_group_tasks[1:]:
                            otask: Task
                            otask.run_status = TaskStatus.RUNNING
                            # 移接 task的前序路径
                            zhc_idx = 0
                            for idx, p in enumerate(task.process_list):
                                zhc_idx = idx
                                dp = copy.deepcopy(p)
                                dp.origin_sku_num = otask.origin_sku_num
                                dp.target_sku_num = dp.origin_sku_num
                                otask.process_list[idx] = dp
                            # 整理当前路径
                            # zhc_process = otask.process_list[zhc_idx]
                            oprocess = otask.process
                            # 开始处理
                            self.process_zb_zx_split(otask)

            # 换包规【换包装作业】, 由纸箱 -> 自制箱，需要进行任务拆分，一般为 1:1 换算
            elif origin_pack_type == PackType.ZX and target_pack_type == PackType.ZZX:
                msg = f'[PC] [换包装作业] 纸箱->自制箱 task: {task.task_id}'
                self.sys_data.logger.info(msg)
                # 纸箱多DC处理
                if task.origin_pack_type == PackType.ZX.value and task.is_master_dc_task:
                    pidx = task.process_list.index(process)
                    # 修正PCS数
                    for tp in task.process_list[:pidx + 1]:
                        tp.set_origin_sku_num(task.origin_sku_num)
                    task.process_list[pidx].target_sku_num = task.origin_sku_num
                    # 处理其他任务
                    for ot in task.ext_group_tasks[1:]:
                        ot: Task
                        ot.run_status = TaskStatus.RUNNING
                        copied_process_list = copy.deepcopy(task.process_list[:pidx + 1])
                        ot.process_list = copied_process_list + ot.process_list[pidx + 1:]
                        # 当前 换包装过程
                        cprocess = copied_process_list[-1]
                        cprocess.target_sku_num = ot.origin_sku_num  # 多DC场景 换包装的target 为原始母任务的origin
                        cprocess.ext_target_lpn = f'v{0}_{strutil.random_str(10)}'[:10]
                        msg = f'[PC] 新任务 task: {ot.task_id}'
                        self.sys_data.logger.info(msg)
                        self.ware_house.distribute_task(TaskCtx(ot))
                # 产生新的LPN
                process.ext_target_lpn = f'v{0}_{strutil.random_str(10)}'[:10]
                self.ware_house.distribute_task(task_ctx)
            elif self.equ_code == DeviceType.ZHC.value:
                # 当为非PTX任务时，经过这里会 修改target_pcs数
                if not task.ext_ptx:
                    process.target_sku_num = task.target_sku_num
                    task.origin_sku_num = task.target_sku_num
                self.ware_house.distribute_task(task_ctx)
            else:
                self.ware_house.distribute_task(task_ctx)

    def process_zb_zx_split(self, task: Task):
        """
        栈板 --> 纸箱 拆分
        去除栈板板转箱后任务拆分模式，改为板转箱后只拆出一个任务，数量是target_num,不需要包规判断
        """
        self.sys_data.logger.info('[PC] [拆板作业] 栈板->件箱 需要拆分子任务 task: %s', task.task_id)
        # [父任务]在工作台要拆分为几个任务就填几
        task.pre_process().operate_pack_num = 1
        task.pre_process().target_sku_num = task.target_sku_num
        # 子任务
        sub_task = copy.deepcopy(task)
        sub_task.lpn = f'v{0}_{strutil.random_str(10)}'[:10]
        sub_task.origin_pack_type = PackType.ZX.value
        sub_task.target_pack_type = task.target_pack_type
        sub_task.origin_sku_num = task.origin_sku_num
        sub_task.target_sku_num = task.target_sku_num
        sub_task.ext_parent_task = task
        sub_task.run_status = TaskStatus.RUNNING
        # 移除 多DC
        sub_task.ext_group_id = None
        sub_task.ext_group_tasks = []
        # 父任务处理
        # [父任务] LPN
        task.pre_process().ext_target_lpn = ', '.join([_.lpn for _ in [sub_task]])
        # [父任务] process_list截取
        task.process_list = task.process_list[:task.process_list.index(task.process)]
        # [父任务] 设置子任务信息
        task.ext_sub_task_list = [sub_task]
        # 对任务进行分发
        for sub_task in task.ext_sub_task_list:
            self.sys_data.logger.info('[PC] 新任务 task: %s', sub_task.task_id)
            self.ware_house.distribute_task(TaskCtx(sub_task))


class SystemCenter(System):
    """
    系统 工作中心
    """
    # 设备列表
    center_list: list[CenterDevice] = []
    center_id_map: dict[str, CenterDevice] = dict()
    center_code_map: dict[str, CenterDevice] = dict()

    ware_house = None
    sys_data: SysDataCtx = None

    def __init__(self, env: Environment, name: str, ware_house, sys_data: SysDataCtx):
        self.center_list: list[CenterDevice] = []
        self.center_id_map: dict[str, CenterDevice] = dict()
        self.center_code_map: dict[str, CenterDevice] = dict()
        #
        super().__init__(env, name)
        self.ware_house = ware_house
        self.sys_data = sys_data
        self.__init_center_list()

    def __init_center_list(self):
        """
        初始化 工作中心设备
        :return:
        """
        # 数据过滤
        center_list = [CenterDevice(self.env, f'Center-{_["equ_code"]}', self.ware_house, self.sys_data, _) for _ in
                       self.sys_data.center_data]
        for c in center_list:
            if self.sys_data.is_valid_equ(c):
                self.center_list.append(c)
        # 初始化
        for c in self.center_list:
            self.center_id_map[c.id] = c
            self.center_code_map[c.device_code()] = c
            # 设备中心 位置LOC
            c.loc_list = [_ for _ in self.sys_data.center_loc_data if _.equ_code == c.device_code()]
            if not c.loc_list:
                raise PcException(f'[PC] 设备位置LOC 数据缺失, {c.equ_code}')
            # 设备处理耗时
            time_cost_dict = self.__time_cost(c.equ_code, self.sys_data)
            if not (time_cost_dict is not None and time_cost_dict.items()):
                raise PcException(f'[PC] 设备耗时 数据缺失, {c.equ_code}')
            c.worktype_time_cost_dict = time_cost_dict

    @staticmethod
    def __time_cost(equ_code: str, sys_data: SysDataCtx) -> dict[int, float]:
        capacity_list = [_ for _ in sys_data.center_capacity_data if _.equ_code == equ_code]
        if not (capacity_list and len(capacity_list) > 0):
            raise PcException(f'[PC] 工作中心 设备能力 数据异常 equ_code={equ_code}, list: {capacity_list}')
        return dict([(_.work_type, SystemCenter.__cost_transform(_.capacity)) for _ in capacity_list])

    @staticmethod
    def __cost_transform(capacity: int) -> float:
        """
        将每小时的工作能力，转换为每工作单位耗时多少秒
        """
        return round(3600 / capacity, 2)

    def run(self):
        while self.running:
            if not self.task_list:
                # FIXME: 会导致任务执行 实际开始时间延后
                yield self.env.timeout(1)
                continue
            task_ctx = self.choose_task()
            # 并发执行 不等待
            self.env.process(self.run_task(task_ctx))

    @staticmethod
    def choose_device(device_list: list[CenterDevice]) -> CenterDevice | None:
        """
        选择设备
        """
        sorted_device_list = sorted(device_list, key=lambda _: len(_.task_list))
        return sorted_device_list[0] if len(sorted_device_list) > 0 else None

    def run_task(self, task_ctx: TaskCtx):
        """
        执行任务  # TODO: 怎么执行任务???
        :param task_ctx:
        :return:
        """
        task: Task = task_ctx.task
        process: TaskProcess = task.process
        step: CommandRouteStep = process.ext_step
        # 可用设备ID
        all_devices = [__ for __ in [self.center_id_map.get(_) for _ in step.ext_equid_list] if __]
        if not all_devices:
            msg = f'[PC] 设备不可用: {self.name}, equ_type: {step.ext_equ_type}({step.ext_equ_type_str})'
            self.sys_data.logger.error(msg)
            return
        # 设备工作类型限制 (仅当有可用设备时 才进行过滤)
        ex_d = all_devices[0]
        equ_type = str(ex_d.equ_type)
        if equ_type in self.sys_data.work_station_dict:
            command_code = task.ext_route.command_type_code
            # 当为 实发实任务时，判断下 GTP设备 是否有设置的 PICK_GTP_REAL 工作类型任务。
            if task.ext_source == TaskSource.DELIVERY and task.ext_delivery_real and equ_type == str(
                    DeviceType.GTP.value):
                equ_station_dict = self.sys_data.work_station_dict[equ_type]
                if 'PICK_GTP_REAL' not in equ_station_dict or not equ_station_dict['PICK_GTP_REAL']:
                    self.sys_data.logger.warning('[PC] 根据工作类型过滤可用设备 (Center) 无可用设备 (实发实)'
                                                 'task: %s (%s)', task.task_id, command_code)
                    task.ext_last_info = f'Center {equ_type} 无实发实[PICK_GTP_REAL]点亮 无可用设备'
                    task.set_run_status(TaskStatus.ERROR, self.sys_data)
                    return
            if command_code in self.sys_data.work_station_dict[equ_type]:
                if task.ext_source == TaskSource.DELIVERY and task.ext_delivery_real and equ_type == str(
                        DeviceType.GTP.value):
                    command_code = 'PICK_GTP_REAL'

                available_work_devices_code = self.sys_data.work_station_dict[equ_type][command_code]
                filtered_available_devices = [ad for ad in all_devices if
                                              ad.device_code() in available_work_devices_code]
                if not filtered_available_devices:
                    self.sys_data.logger.warning(
                        f'[PC] 根据工作类型过滤可用设备 (Center) 无可用设备 '
                        f'task： {task.task_id} ({command_code}), '
                        f'equ_type: {equ_type}, '
                        f'before: {[_.equ_code for _ in all_devices]},'
                        f'after: {[_.equ_code for _ in filtered_available_devices]}')
                    task.ext_last_info = f'Center {equ_type} (workType) 无可用设备'
                    task.set_run_status(TaskStatus.ERROR, self.sys_data)
                    return
                self.sys_data.logger.debug(
                    f'[PC] 根据工作类型过滤可用设备 (Center) '
                    f'task： {task.task_id} ({command_code}), '
                    f'equ_type: {equ_type}, '
                    f'before: {[_.equ_code for _ in all_devices]},'
                    f'after: {[_.equ_code for _ in filtered_available_devices]}')
                all_devices = filtered_available_devices
            else:
                all_devices = []
                self.sys_data.logger.warning(
                    f'[PC] 根据工作类型过滤可用设备 (Center) 无可用设备 - 未配置workType'
                    f'task： {task.task_id} ({command_code}), '
                    f'equ_type: {equ_type}, '
                    f'before: {[_.equ_code for _ in all_devices]},'
                    f'after: {[]}')
                task.ext_last_info = f'Center {equ_type} (workType) 未配置该类型workType, 无可用设备'
                task.set_run_status(TaskStatus.ERROR, self.sys_data)
                return
        # 选择不同的设备进行分发任务
        device = self.choose_device(all_devices)
        device.push_task(task_ctx)
        yield self.env.timeout(0)

    def start(self):
        super().start()
        self.running = True
        for cd in self.center_list:
            cd.start()

    def stop(self):
        for cd in self.center_list:
            cd.stop()
        self.running = False
        super().stop()
